/**
 * Copyright 1998-2006, CHISEL Group, University of Victoria, Victoria, BC, Canada.
 * All rights reserved.
 */
package ca.uvic.cs.cogz.util;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.swing.JTree;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

import edu.stanford.smi.protege.model.Cls;
import edu.stanford.smi.protege.model.Frame;
import edu.stanford.smi.protege.util.ComponentUtilities;
import edu.stanford.smi.protege.util.LazyTreeNode;
import edu.stanford.smi.protegex.owl.model.RDFProperty;
import edu.stanford.smi.protegex.owl.model.impl.DefaultOWLNamedClass;
import edu.stanford.smi.protegex.owl.model.impl.DefaultRDFSNamedClass;

/**
 * Utility class for handling JTree related operations.
 * 
 * @author Sean Falconer
 * @date 10-Jan-07
 */
public class TreeUtil {
	private static Hashtable<Collection<Frame>, Collection<Collection<Frame>>> objectPathsCache = new Hashtable<Collection<Frame>, Collection<Collection<Frame>>>();
	private static Hashtable<Collection<Frame>, TreePath> objectToTreePathCache = new Hashtable<Collection<Frame>, TreePath>();
	private static Hashtable<JTree, TreeModel> treeToModelCache = new Hashtable<JTree, TreeModel>();
	
	public static void resetCachedObjects() {
		objectPathsCache.clear();
		objectToTreePathCache.clear();
		treeToModelCache.clear();
	}

	/**
	 * Determine is the array of tree paths contains the given path.
	 * 
	 * @return True if the array contains the path.
	 */
	public static boolean pathContains(TreePath[] treePaths, TreePath path) {
		if (treePaths != null) {
			for (TreePath treePath : treePaths) {
				if (treePath.equals(path)) {
					return true;
				}
			}
		}
		return false;
	}

	public static Object getLastSelectedUserObject(JTree tree) {
		TreePath[] paths = tree.getSelectionPaths();

		if (paths != null && paths.length > 0) {
			TreePath treePath = paths[paths.length - 1];

			return ((LazyTreeNode) treePath.getLastPathComponent()).getUserObject();
		}

		return null;
	}

	public static Frame getLastSelectedUserObject(TreePath treePath) {
		if (treePath != null) {
			LazyTreeNode node = (LazyTreeNode) treePath.getLastPathComponent();
			if(node.getUserObject() instanceof Frame) {
				return (Frame) node.getUserObject();
			}
		}

		return null;
	}

	/**
	 * Expands all the object paths in the given tree.
	 */
	@SuppressWarnings("unchecked")
	public static void expandPaths(JTree tree, Collection<Collection> paths) {
		if (paths != null) {
			for (Collection objectPath : paths) {
				try {
					ComponentUtilities.setExpanded(tree, objectPath, true);
				} catch (NullPointerException e) {
					System.out.println("expand paths error");
				}
			}
		}
	}

	/**
	 * Expands all the given TreePath objects for the given tree.
	 */
	public static void expandPaths(JTree tree, ArrayList<TreePath> paths) {
		if (paths != null) {
			for (TreePath path : paths) {
				tree.expandPath(path);
				tree.makeVisible(path);
			}
		}

		tree.updateUI();
	}

	/**
	 * Expands the tree with the given path and selects the last element in the path.
	 */
	public static void expandAndSelectPath(JTree tree, TreePath path) {
		tree.expandPath(path);
		tree.setSelectionPath(path);
		tree.scrollPathToVisible(path);
		tree.updateUI();
	}

	/**
	 * Compares whether two TreePath objects are equal based on the object's stored in their user
	 * object field.
	 */
	public static boolean equals(TreePath treePath, Collection<Frame> objectPath) {
		if (treePath.getPathCount() - 1 != objectPath.size()) {
			return false;
		}

		Object[] objPath1 = treePath.getPath();
		Object[] objPath2 = objectPath.toArray();

		for (int i = 1; i < objPath1.length; i++) {
			LazyTreeNode node = (LazyTreeNode) objPath1[i];
			Frame frame = (Frame) objPath2[i - 1];

			Frame treeFrame = FrameUtil.getFrameFromObject(node.getUserObject());

			if (treeFrame == null || !treeFrame.equals(frame)) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Builds all the possible paths to the root Cls for a given Cls. A Cls can have multiple paths
	 * to the root due to multiple inheritance, so we have to recursively build all the paths.
	 */
	@SuppressWarnings("unchecked")
	public static void buildObjectPaths(Cls currentCls, ArrayList<Frame> currentPath, Collection<Collection> objectPaths, boolean ignoreLeafNodes) {
		if (!ignoreLeafNodes || (ignoreLeafNodes && currentCls.getDirectSubclassCount() > 0)) {
			currentPath.add(0, currentCls);
		}

		if (currentCls.getDirectSuperclassCount() == 0) {
			objectPaths.add((ArrayList<Frame>) currentPath.clone());
		} else {
			for (Iterator<Cls> iter = currentCls.getDirectSuperclasses().iterator(); iter.hasNext();) {
				Cls parentCls = iter.next();
				// HACK : should figure out how to know if the Cls is in the tree
				
				// FIXME : this won't work for non-OWL ontologies
				if (parentCls instanceof DefaultOWLNamedClass || parentCls instanceof DefaultRDFSNamedClass) {
					buildObjectPaths(parentCls, currentPath, objectPaths, ignoreLeafNodes);
					currentPath.remove(0);
				}
			}
		}
	}

	/**
	 * Saves the current selection of the tree. Each selected {@link TreePath} is converted into a
	 * collection of user objects that maps to the {@link TreePath}.
	 * 
	 * @param tree
	 * @return a collection containing collections of user path objects that map to {@link TreePath}s.
	 * @see ComponentUtilities
	 */
	@SuppressWarnings("unchecked")
	public static Collection<Collection> saveSelection(JTree tree) {
		Collection<Collection> selectedUserPaths = Collections.emptyList();
		TreePath[] paths = tree.getSelectionPaths();
		if (paths != null) {
			selectedUserPaths = new ArrayList<Collection>(paths.length);
			for (int i = 0; i < paths.length; i++) {
				TreePath path = paths[i];
				Collection objPath = ComponentUtilities.getObjectPath(path);
				selectedUserPaths.add(objPath);
			}
		}
		return selectedUserPaths;
	}
	
	/**
	 * Saves the expanded state of the tree. The expanded {@link TreePath} objects are converted to
	 * a {@link Collection} of user objects.
	 * 
	 * @param tree the tree to save the expansion
	 * @return a collection containing a collection of user objects that map to {@link TreePath}s.
	 * @see ComponentUtilities
	 */
	@SuppressWarnings("unchecked")
	public static Collection<Collection> saveExpansion(JTree tree) {
		Enumeration<TreePath> paths = tree.getExpandedDescendants(new TreePath(tree.getModel().getRoot()));
		Collection<Collection> expandedUserPaths = new ArrayList<Collection>();
		while ((paths != null) && paths.hasMoreElements()) {
			TreePath path = paths.nextElement();
			Collection col = ComponentUtilities.getObjectPath(path);
			expandedUserPaths.add(col);
		}
		return expandedUserPaths;
	}

	/**
	 * Restores the selection to a tree.
	 * 
	 * @param tree
	 * @param selectedUserPaths a collection containing of a list of user objects that map to
	 *            {@link TreePath}s.
	 * @see ComponentUtilities
	 */
	@SuppressWarnings("unchecked")
	public static void restoreSelection(JTree tree, Collection<Collection> selectedUserPaths) {
		ComponentUtilities.setSelectedObjectPaths(tree, selectedUserPaths);
	}

	public static ArrayList<TreePath> getTreePaths(JTree tree, Collection<Frame> frames, boolean ignoreLeafNodes) {
		Collection<Collection<Frame>> objectPaths = objectPathsCache.get(frames);

		if (objectPaths != null && !ignoreLeafNodes) {
			ArrayList<TreePath> treePaths = new ArrayList<TreePath>();
			for (Collection<Frame> objectPath : objectPaths) {
				TreePath path = getTreePath(tree, objectPath);
				if (path != null) {
					treePaths.add(getTreePath(tree, objectPath));
				}
			}
			return treePaths;
		} else {
			ArrayList<TreePath> paths = new ArrayList<TreePath>();
			ArrayList<Frame> pathFrame = new ArrayList<Frame>();
			objectPaths = new ArrayList<Collection<Frame>>();
			for (Frame frame : frames) {
				buildTreePaths((Cls) frame, tree, pathFrame, paths, ignoreLeafNodes, objectPaths);
				pathFrame.clear();
			}

			if (!ignoreLeafNodes) {
				objectPathsCache.put(frames, objectPaths);
			}

			return paths;
		}
	}

	/**
	 * Builds all the possible TreePath object's for a given Cls. A Cls can have multiple paths to
	 * the root due to multiple inheritance, so we have to recursively build all the paths.
	 */
	@SuppressWarnings("unchecked")
	public static void buildTreePaths(Cls currentCls, JTree tree, ArrayList<Frame> currentPath, Collection<TreePath> treePaths, boolean ignoreLeafNodes, Collection<Collection<Frame>> objectPaths) {
		if (!ignoreLeafNodes || (ignoreLeafNodes && currentCls.getDirectSubclassCount() > 0)) {
			currentPath.add(0, currentCls);
		}
		if (currentCls.getDirectSuperclassCount() == 0) {
			objectPaths.add((ArrayList<Frame>) currentPath.clone());
			TreePath path = getTreePath(tree, currentPath);
			if (path != null) {
				treePaths.add(getTreePath(tree, currentPath));
			}
		} else {
			for (Iterator<Cls> iter = currentCls.getDirectSuperclasses().iterator(); iter.hasNext();) {
				Cls parentCls = iter.next();
				buildTreePaths(parentCls, tree, currentPath, treePaths, ignoreLeafNodes, objectPaths);
				currentPath.remove(0);
			}
		}
	}

	public static TreePath getTreePath(LazyTreeNode lazyTreeNode) {
		LinkedList<LazyTreeNode> list = new LinkedList<LazyTreeNode>();

		while (lazyTreeNode != null) {
			list.addFirst(lazyTreeNode);
			lazyTreeNode = lazyTreeNode.getLazyTreeNodeParent();
		}

		return new TreePath(list.toArray());
	}
	
	/**
	 * Attempts to reconstruct the slot TreePath.  Slots are stored under their associated domain frame element in the 
	 * tree.  We get the domain and reconstruct the path to that domain object and then find the LazyTreeNode
	 * matching the given slot and add that to the domain class tree path.
	 */
	@SuppressWarnings("unchecked")
	private static TreePath getSlotTreePath(JTree tree, Frame frame) {
		RDFProperty property = (RDFProperty)frame;
		Collection domain = property.getDomains(false);
		
		Cls cls = (Cls)((List)domain).get(0);
		List<Frame> temp = new ArrayList<Frame>();
		while(cls != null) {
			temp.add(0, cls);
			
			boolean found = false;
			for(Object o : cls.getSuperclasses()) {
				if(o instanceof DefaultOWLNamedClass) {
					cls = (Cls)o;
					found = true;
					break;
				}
			}
			
			if(!found) cls = null;
		}
		
		TreePath domainTreePath = getTreePath(tree, temp);
		if(domainTreePath == null) return null;
		
		LazyTreeNode node = (LazyTreeNode) domainTreePath.getLastPathComponent();
		
		if(node == null || node.getChildCount() == 0) {
			System.out.println();
			return null;
		}
		
		domainTreePath = domainTreePath.pathByAddingChild(node.getChildAt(0));
		
		for(int i = 0; i < node.getChildAt(0).getChildCount(); i++) {
			LazyTreeNode childNode = (LazyTreeNode)node.getChildAt(0).getChildAt(i);
			if(childNode.getUserObject().equals(frame)) {
				domainTreePath = domainTreePath.pathByAddingChild(childNode);
				break;
			}
		}
		
		return domainTreePath;
		
//		LazyTreeNode nextNode = (LazyTreeNode)pathNode.getChildAt(0);
//
//		foundPath = true;
//		list.add(nextNode);
//		list.add(getChildNode(nextNode, frame));
//		break;
	}

	/**
	 * Constructs a TreePath object based on the path represented by a Collection of Cls objects.
	 */
	public static TreePath getTreePath(JTree tree, Collection<Frame> path) {
		TreePath treePath = getCachedTreePath(tree, path);

		if (treePath == null) {
			LinkedList<LazyTreeNode> list = new LinkedList<LazyTreeNode>();
			LazyTreeNode pathNode = (LazyTreeNode) tree.getModel().getRoot();
			list.add(pathNode);

			boolean foundPath = false;

			for (Frame frame : path) {
				if(!(frame instanceof Cls)) {
					return getSlotTreePath(tree, frame);
				}
				LazyTreeNode nextNode = getChildNode(pathNode, frame);

				// try the next node, this is because the object hierarchy may not be
				// identical to the tree hierarchy due to tree node filtering
				if (nextNode == null && !foundPath) {
					continue;
				}

				foundPath = true;
				pathNode = nextNode;
				list.add(pathNode);
			}

			Object[] pathArray = path.toArray();

			if (list.size() > 0 && list.get(list.size() - 1) != null && foundPath) {
				// System.out.println(list.get(list.size() - 1).getClass());
				Object userObject = list.get(list.size() - 1).getUserObject();
				if (userObject != null && userObject.equals(pathArray[pathArray.length - 1])) {
					treePath = new TreePath(list.toArray());
					objectToTreePathCache.put(path, treePath);
					return treePath;
				}
			}
			// list.get(list.size() - 1).getUserObject().equals(pathArray[pathArray.length - 1])) {
			// only return the path if we found a valid one
			// if (foundPath) {
			// return new TreePath(list.toArray());
			// }
			// }
		}

		//System.out.println(tree.getModel().

		return treePath;
	}

	public static LazyTreeNode getChildNode(LazyTreeNode node, Object userObject) {
		if (node != null) {
			int nChildren = node.getChildCount();
			for (int i = 0; i < nChildren; ++i) {
				LazyTreeNode childNode = (LazyTreeNode) node.getChildAt(i);
				if (childNode == null || childNode.getUserObject() == null || userObject == null) {
					continue;
				}
				if (childNode.getUserObject().equals(userObject)) {
					return childNode;
				}
			}
		}
		return null;
	}

	private static TreePath getCachedTreePath(JTree tree, Collection<Frame> path) {
		if (path == null) {
			return null;
		}

		TreeModel treeModel = treeToModelCache.get(tree);
		TreePath treePath = null;
		if (treeModel == null) {
			treeToModelCache.put(tree, tree.getModel());

			// listen to model changes so I can clear the cache
			// this is for when filters are applied or removed and the cached tree paths are no longer valid
			tree.getModel().addTreeModelListener(new TreeModelListener() {
				public void treeNodesChanged(TreeModelEvent e) {}

				public void treeNodesInserted(TreeModelEvent e) {}

				public void treeNodesRemoved(TreeModelEvent e) {}

				public void treeStructureChanged(TreeModelEvent e) {
					objectToTreePathCache.clear();
				}

			});
		} else {
			treePath = objectToTreePathCache.get(path);
		}

		return treePath;
	}
}
